/*============================================================== TIMER V1.0 FINAL by mircemk Oct. 2026 CrowPanel 2.1" ESP32-S3 Rotary Display 480 x 480 ESP32 Core : 2.0.17 Arduino_GFX : 1.4.4 TIMER RANGES ------------------------------------------------------------ 1 MIN - Yellow 10 MIN - Turquoise / Blue-Green 30 MIN - Orange 60 MIN - Magenta TOUCH ------------------------------------------------------------ Lower-center touch zone: 1 MIN -> 10 MIN -> 30 MIN -> 60 MIN -> 1 MIN ENCODER ------------------------------------------------------------ Stable SLOW-CLEAN polling algorithm CLK = GPIO4 DT = GPIO42 SHORT CLICK: START / STOP / RESUME LONG CLICK (~1.2 s): RESET -> 00:00 Current range remains selected BUZZER ------------------------------------------------------------ External PCF8574 = 0x20 P0 -> buzzer (-) buzzer (+) -> 3.3V P0 LOW = buzzer ON P0 HIGH = buzzer OFF TIMER FINISHED ------------------------------------------------------------ RED + BEEP 500 ms WHITE + SILENT 500 ms repeated 3 times ==============================================================*/ #include #include #include #include #include // ============================================================ // DISPLAY // ============================================================ #define DISPLAY_WIDTH 480 #define DISPLAY_HEIGHT 480 #define CX 240 #define CY 240 // ============================================================ // DISPLAY PINS // ============================================================ #define PCLK_NEG 1 #define BL_PIN 6 #define PANEL_CS 16 #define PANEL_SCK 2 #define PANEL_SDA 1 // ============================================================ // ROTARY ENCODER // ============================================================ #define ENCODER_CLK 4 #define ENCODER_DT 42 // ============================================================ // I2C // ============================================================ #define I2C_SDA 38 #define I2C_SCL 39 // Built-in PCF8574 #define INTERNAL_PCF_ADDR 0x21 // External PCF8574 #define EXTERNAL_PCF_ADDR 0x20 // Touch #define TOUCH_ADDR 0x15 // ============================================================ // BUZZER // External PCF8574 P0 // ============================================================ #define BUZZER_BIT 0 uint8_t externalPcfState = 0xFF; // ============================================================ // DISPLAY OBJECTS // ============================================================ Arduino_DataBus *panelBus = nullptr; Arduino_ESP32RGBPanel *rgbpanel = nullptr; Arduino_RGB_Display *display = nullptr; Arduino_Canvas *gfx = nullptr; extern const uint8_t st7701_type7_init_operations[]; // ============================================================ // TOUCH // ============================================================ Adafruit_CST8XX touch = Adafruit_CST8XX(); bool touchWasDown = false; bool rangeTouchCandidate = false; unsigned long lastTouchPollMillis = 0; const unsigned long TOUCH_POLL_MS = 20; // ============================================================ // COLORS // ============================================================ #define COL_BLACK 0x0000 #define COL_WHITE 0xFFFF #define COL_RED 0xF800 // Range colors #define COL_1MIN 0xFFE0 // Yellow #define COL_10MIN 0x06F7 // Turquoise #define COL_30MIN 0xFD20 // Orange #define COL_60MIN 0xF81F // Magenta // ============================================================ // TIMER GEOMETRY // ============================================================ const int OUTER_RADIUS = 235; const int SEPARATOR_RADIUS = 203; const int INNER_RADIUS = 200; const int TICK_OUTER_RADIUS = 228; const int TICK_MINOR_INNER = 216; const int TICK_MAJOR_INNER = 211; const int NUMBER_RADIUS = 174; // ============================================================ // TOUCH ZONE FOR RANGE SELECTION // ============================================================ const int RANGE_TOUCH_X1 = 115; const int RANGE_TOUCH_X2 = 365; const int RANGE_TOUCH_Y1 = 290; const int RANGE_TOUCH_Y2 = 435; // ============================================================ // TIMER RANGES // ============================================================ enum TimerRange { RANGE_1_MIN, RANGE_10_MIN, RANGE_30_MIN, RANGE_60_MIN }; TimerRange currentRange = RANGE_1_MIN; // ============================================================ // TIMER VALUE / STATE // ============================================================ int remainingSeconds = 0; bool timerRunning = false; bool countdownHasStarted = false; unsigned long lastSecondMillis = 0; // ============================================================ // STABLE ENCODER SETTINGS // ============================================================ const int ENCODER_DIRECTION = -1; const unsigned long STABLE_TIME_MS = 8; const unsigned long CLICK_LOCKOUT_MS = 70; // ============================================================ // ENCODER STATE // ============================================================ int lastRawCLK = HIGH; int stableCLK = HIGH; unsigned long clkChangeTime = 0; unsigned long lastClickTime = 0; bool readyForNextClick = true; // ============================================================ // BUTTON STATE // ============================================================ bool rawButtonLast = false; bool stableButtonState = false; unsigned long buttonDebounceMillis = 0; const unsigned long BUTTON_DEBOUNCE_MS = 40; const unsigned long BUTTON_POLL_MS = 5; unsigned long lastButtonPollMillis = 0; // ============================================================ // LONG PRESS // ============================================================ const unsigned long LONG_PRESS_MS = 1200; unsigned long buttonPressStartMillis = 0; bool longPressHandled = false; // ============================================================ // RANGE HELPERS // ============================================================ int getMaxSeconds() { switch (currentRange) { case RANGE_1_MIN: return 60; case RANGE_10_MIN: return 600; case RANGE_30_MIN: return 1800; case RANGE_60_MIN: return 3600; } return 60; } // ============================================================ // ENCODER STEP // ============================================================ int getEncoderStepSeconds() { switch (currentRange) { case RANGE_1_MIN: // 60 fields return 1; case RANGE_10_MIN: // 50 fields // 600 / 50 = 12 sec return 12; case RANGE_30_MIN: // 30 fields // 1800 / 30 = 60 sec return 60; case RANGE_60_MIN: // 60 fields // 3600 / 60 = 60 sec return 60; } return 1; } // ============================================================ // RANGE COLOR // ============================================================ uint16_t getRingColor() { switch (currentRange) { case RANGE_1_MIN: return COL_1MIN; case RANGE_10_MIN: return COL_10MIN; case RANGE_30_MIN: return COL_30MIN; case RANGE_60_MIN: return COL_60MIN; } return COL_1MIN; } // ============================================================ // RANGE TEXT // ============================================================ const char *getRangeText() { switch (currentRange) { case RANGE_1_MIN: return "1 MIN"; case RANGE_10_MIN: return "10 MIN"; case RANGE_30_MIN: return "30 MIN"; case RANGE_60_MIN: return "60 MIN"; } return "1 MIN"; } // ============================================================ // POLAR COORDINATES // ============================================================ void polarPoint(float angleDeg, int radius, int &x, int &y) { float a = angleDeg * PI / 180.0f; x = CX + round(radius * sin(a)); y = CY - round(radius * cos(a)); } // ============================================================ // RADIAL SCALE TICK // ============================================================ void drawRadialTick(float angleDeg, int innerRadius, int outerRadius, float thickness, uint16_t color) { float a = angleDeg * PI / 180.0f; float rx = sin(a); float ry = -cos(a); float px = cos(a); float py = sin(a); float halfThickness = thickness / 2.0f; float ix = CX + innerRadius * rx; float iy = CY + innerRadius * ry; float ox = CX + outerRadius * rx; float oy = CY + outerRadius * ry; int x1 = round(ix + px * halfThickness); int y1 = round(iy + py * halfThickness); int x2 = round(ix - px * halfThickness); int y2 = round(iy - py * halfThickness); int x3 = round(ox - px * halfThickness); int y3 = round(oy - py * halfThickness); int x4 = round(ox + px * halfThickness); int y4 = round(oy + py * halfThickness); gfx->fillTriangle( x1, y1, x2, y2, x3, y3, color ); gfx->fillTriangle( x1, y1, x3, y3, x4, y4, color ); } // ============================================================ // BOLD TEXT // ============================================================ void drawBoldText(const char *text, int x, int y, int textSize, uint16_t color) { gfx->setTextSize( textSize ); gfx->setTextColor( color ); gfx->setCursor(x, y); gfx->print(text); gfx->setCursor(x + 1, y); gfx->print(text); gfx->setCursor(x, y + 1); gfx->print(text); gfx->setCursor(x + 1, y + 1); gfx->print(text); } // ============================================================ // CENTERED BOLD TEXT // ============================================================ void drawCenteredBoldText(const char *text, int centerX, int y, int textSize, uint16_t color) { int textWidth = strlen(text) * 6 * textSize; int x = centerX - textWidth / 2; drawBoldText( text, x, y, textSize, color ); } // ============================================================ // GENERIC SCALE // ============================================================ void drawGenericScale(int totalMarks, int marksPerMajor) { float angleStep = 360.0f / (float)totalMarks; for (int i = 0; i < totalMarks; i++) { float angle = i * angleStep; bool major = (i % marksPerMajor == 0); if (major) { drawRadialTick( angle, TICK_MAJOR_INNER, TICK_OUTER_RADIUS, 6.0f, COL_BLACK ); } else { drawRadialTick( angle, TICK_MINOR_INNER, TICK_OUTER_RADIUS, 3.0f, COL_BLACK ); } } } // ============================================================ // DRAW SCALE // ============================================================ void drawScale() { switch (currentRange) { case RANGE_1_MIN: drawGenericScale( 60, 5 ); break; case RANGE_10_MIN: drawGenericScale( 50, 5 ); break; case RANGE_30_MIN: drawGenericScale( 30, 5 ); break; case RANGE_60_MIN: drawGenericScale( 60, 5 ); break; } } // ============================================================ // DRAW ONE SCALE NUMBER // ============================================================ void drawScaleNumber(float angle, int value) { int x; int y; polarPoint( angle, NUMBER_RADIUS, x, y ); char text[5]; sprintf( text, "%d", value ); const int textSize = 3; int textWidth = strlen(text) * 6 * textSize; int textHeight = 8 * textSize; drawBoldText( text, x - textWidth / 2, y - textHeight / 2, textSize, COL_BLACK ); } // ============================================================ // DRAW SCALE NUMBERS // ============================================================ void drawNumbers() { // ========================================================== // 1 MIN // 0,55,50 ... 5 // ========================================================== if (currentRange == RANGE_1_MIN) { for (int pos = 0; pos < 12; pos++) { float angle = pos * 30.0f; int value; if (pos == 0) { value = 0; } else { value = 60 - pos * 5; } drawScaleNumber( angle, value ); } return; } // ========================================================== // 10 MIN // 0,9,8 ... 1 // ========================================================== if (currentRange == RANGE_10_MIN) { for (int pos = 0; pos < 10; pos++) { float angle = pos * 36.0f; int value; if (pos == 0) { value = 0; } else { value = 10 - pos; } drawScaleNumber( angle, value ); } return; } // ========================================================== // 30 MIN // 0,25,20,15,10,5 // ========================================================== if (currentRange == RANGE_30_MIN) { for (int pos = 0; pos < 6; pos++) { float angle = pos * 60.0f; int value; if (pos == 0) { value = 0; } else { value = 30 - pos * 5; } drawScaleNumber( angle, value ); } return; } // ========================================================== // 60 MIN // 0,55,50 ... 5 // ========================================================== if (currentRange == RANGE_60_MIN) { for (int pos = 0; pos < 12; pos++) { float angle = pos * 30.0f; int value; if (pos == 0) { value = 0; } else { value = 60 - pos * 5; } drawScaleNumber( angle, value ); } } } // ============================================================ // RED REMAINING TIME SECTOR // ============================================================ void drawRemainingRedSector() { int maxSeconds = getMaxSeconds(); if (remainingSeconds <= 0) { return; } if (remainingSeconds >= maxSeconds) { gfx->fillCircle( CX, CY, INNER_RADIUS, COL_RED ); return; } float redAngle = ((float)remainingSeconds / (float)maxSeconds) * 360.0f; float startAngle = 360.0f - redAngle; for (float angle = startAngle; angle < 360.0f; angle += 0.7f) { int x1; int y1; int x2; int y2; polarPoint( angle, INNER_RADIUS, x1, y1 ); polarPoint( angle + 1.0f, INNER_RADIUS, x2, y2 ); gfx->fillTriangle( CX, CY, x1, y1, x2, y2, COL_RED ); } } // ============================================================ // FORMAT TIMER // ============================================================ void formatTimer(char *buffer) { int minutes = remainingSeconds / 60; int seconds = remainingSeconds % 60; sprintf( buffer, "%02d:%02d", minutes, seconds ); } // ============================================================ // COMPLETE TIMER FRAME // ============================================================ void renderTimerFrame() { gfx->fillScreen( COL_BLACK ); // Colored range ring gfx->fillCircle( CX, CY, OUTER_RADIUS, getRingColor() ); // Separator gfx->fillCircle( CX, CY, SEPARATOR_RADIUS, COL_BLACK ); // White timer area gfx->fillCircle( CX, CY, INNER_RADIUS, COL_WHITE ); // Red remaining area drawRemainingRedSector(); drawScale(); drawNumbers(); char timerText[8]; formatTimer( timerText ); drawCenteredBoldText( timerText, CX, 184, 7, COL_BLACK ); drawCenteredBoldText( getRangeText(), CX, 315, 4, COL_BLACK ); // Flicker-free screen update gfx->flush(); } // ============================================================ // ALARM FLASH FRAME // ============================================================ void renderFlashFrame(uint16_t innerColor) { gfx->fillScreen( COL_BLACK ); gfx->fillCircle( CX, CY, OUTER_RADIUS, getRingColor() ); gfx->fillCircle( CX, CY, SEPARATOR_RADIUS, COL_BLACK ); gfx->fillCircle( CX, CY, INNER_RADIUS, innerColor ); drawScale(); drawNumbers(); drawCenteredBoldText( "00:00", CX, 184, 7, COL_BLACK ); drawCenteredBoldText( getRangeText(), CX, 315, 4, COL_BLACK ); gfx->flush(); } // ============================================================ // INTERNAL PCF8574 // Address 0x21 // ============================================================ void internalPcfWrite(uint8_t value) { Wire.beginTransmission( INTERNAL_PCF_ADDR ); Wire.write( value ); Wire.endTransmission(); } uint8_t internalPcfRead() { Wire.requestFrom( INTERNAL_PCF_ADDR, (uint8_t)1 ); if (Wire.available()) { return Wire.read(); } return 0xFF; } // ============================================================ // EXTERNAL PCF8574 // Address 0x20 // ============================================================ void externalPcfWrite() { Wire.beginTransmission( EXTERNAL_PCF_ADDR ); Wire.write( externalPcfState ); Wire.endTransmission(); } // ============================================================ // BUZZER CONTROL // // Buzzer + = 3.3V // Buzzer - = P0 // // LOW = ON // HIGH = OFF // ============================================================ void buzzerON() { externalPcfState &= ~(1 << BUZZER_BIT); externalPcfWrite(); } void buzzerOFF() { externalPcfState |= (1 << BUZZER_BIT); externalPcfWrite(); } // ============================================================ // TOUCH RESET // Internal PCF8574 P0 // ============================================================ void resetTouch() { internalPcfWrite( 0xFF ); delay( 20 ); // P0 LOW internalPcfWrite( 0xFE ); delay( 30 ); // P0 HIGH internalPcfWrite( 0xFF ); delay( 150 ); } // ============================================================ // ENCODER BUTTON // Internal PCF8574 P5 // ============================================================ bool isEncoderButtonPressed() { uint8_t state = internalPcfRead(); return !(state & (1 << 5)); } // ============================================================ // RANGE TOUCH ZONE // ============================================================ bool isRangeTouchZone(int x, int y) { return x >= RANGE_TOUCH_X1 && x <= RANGE_TOUCH_X2 && y >= RANGE_TOUCH_Y1 && y <= RANGE_TOUCH_Y2; } // ============================================================ // RESET TIMER // // LONG PRESS // ============================================================ void resetTimer() { timerRunning = false; countdownHasStarted = false; remainingSeconds = 0; lastSecondMillis = millis(); // Ensure alarm is silent buzzerOFF(); // Re-sync encoder lastRawCLK = digitalRead( ENCODER_CLK ); stableCLK = lastRawCLK; clkChangeTime = millis(); lastClickTime = millis(); readyForNextClick = true; // Touch state touchWasDown = false; rangeTouchCandidate = false; renderTimerFrame(); Serial.println( "TIMER RESET -> 00:00" ); } // ============================================================ // CHANGE TIMER RANGE // ============================================================ void changeTimerRange() { if (countdownHasStarted) { return; } switch (currentRange) { case RANGE_1_MIN: currentRange = RANGE_10_MIN; break; case RANGE_10_MIN: currentRange = RANGE_30_MIN; break; case RANGE_30_MIN: currentRange = RANGE_60_MIN; break; case RANGE_60_MIN: currentRange = RANGE_1_MIN; break; } remainingSeconds = 0; buzzerOFF(); // Re-sync encoder lastRawCLK = digitalRead( ENCODER_CLK ); stableCLK = lastRawCLK; clkChangeTime = millis(); lastClickTime = millis(); readyForNextClick = true; renderTimerFrame(); Serial.print( "RANGE: " ); Serial.println( getRangeText() ); } // ============================================================ // TOUCH HANDLER // ============================================================ void handleTouch() { unsigned long now = millis(); if ( now - lastTouchPollMillis < TOUCH_POLL_MS ) { return; } lastTouchPollMillis = now; uint8_t touches = touch.touched(); // ---------------------------------------------------------- // TOUCH DOWN // ---------------------------------------------------------- if (touches > 0) { CST_TS_Point p = touch.getPoint(0); if (!touchWasDown) { touchWasDown = true; rangeTouchCandidate = !countdownHasStarted && isRangeTouchZone( p.x, p.y ); } return; } // ---------------------------------------------------------- // RELEASE // ---------------------------------------------------------- if (touchWasDown) { touchWasDown = false; if (rangeTouchCandidate) { rangeTouchCandidate = false; changeTimerRange(); } else { rangeTouchCandidate = false; } } } // ============================================================ // STABLE DT MAJORITY READ // // BEST WORKING ENCODER METHOD // ============================================================ int readStableDT() { int highCount = 0; int lowCount = 0; for (int i = 0; i < 7; i++) { if ( digitalRead( ENCODER_DT ) ) { highCount++; } else { lowCount++; } delayMicroseconds( 250 ); } return (highCount > lowCount) ? HIGH : LOW; } // ============================================================ // ACCEPT ENCODER CLICK // ============================================================ void acceptEncoderClick(int direction) { if (countdownHasStarted) { return; } int oldValue = remainingSeconds; int stepSeconds = getEncoderStepSeconds(); remainingSeconds += direction * stepSeconds; int maxSeconds = getMaxSeconds(); if (remainingSeconds < 0) { remainingSeconds = 0; } if (remainingSeconds > maxSeconds) { remainingSeconds = maxSeconds; } if ( remainingSeconds != oldValue ) { renderTimerFrame(); Serial.print( "SET: " ); Serial.print( remainingSeconds ); Serial.print( " sec " ); Serial.println( getRangeText() ); } } // ============================================================ // SLOW-CLEAN ENCODER // // DO NOT MODIFY // ============================================================ void handleEncoder() { unsigned long now = millis(); int rawCLK = digitalRead( ENCODER_CLK ); // Raw CLK changed if ( rawCLK != lastRawCLK ) { lastRawCLK = rawCLK; clkChangeTime = now; } // Stable for at least 8 ms if ( rawCLK != stableCLK && (now - clkChangeTime) >= STABLE_TIME_MS ) { stableCLK = rawCLK; // -------------------------------------------------------- // Stable falling edge // -------------------------------------------------------- if ( stableCLK == LOW && readyForNextClick && (now - lastClickTime) >= CLICK_LOCKOUT_MS ) { int dt = readStableDT(); int direction; if (dt == HIGH) { direction = +1; } else { direction = -1; } direction *= ENCODER_DIRECTION; acceptEncoderClick( direction ); lastClickTime = now; readyForNextClick = false; } // -------------------------------------------------------- // Re-arm after CLK returns HIGH // -------------------------------------------------------- if ( stableCLK == HIGH ) { readyForNextClick = true; } } } // ============================================================ // SHORT CLICK // START / STOP / RESUME // ============================================================ void encoderButtonAction() { // ---------------------------------------------------------- // RUNNING -> PAUSE // ---------------------------------------------------------- if (timerRunning) { timerRunning = false; Serial.println( "TIMER PAUSED" ); return; } // ---------------------------------------------------------- // PAUSED -> RESUME // ---------------------------------------------------------- if (countdownHasStarted) { timerRunning = true; lastSecondMillis = millis(); Serial.println( "TIMER RESUMED" ); return; } // ---------------------------------------------------------- // NEW START // ---------------------------------------------------------- if (remainingSeconds > 0) { countdownHasStarted = true; timerRunning = true; lastSecondMillis = millis(); Serial.println( "TIMER STARTED" ); } } // ============================================================ // BUTTON HANDLER // // SHORT click -> action on RELEASE // LONG click -> RESET after 1.2 sec // ============================================================ void handleButton() { unsigned long now = millis(); if ( now - lastButtonPollMillis < BUTTON_POLL_MS ) { return; } lastButtonPollMillis = now; bool rawState = isEncoderButtonPressed(); // Raw state changed if ( rawState != rawButtonLast ) { rawButtonLast = rawState; buttonDebounceMillis = now; } // Debounced state if ( now - buttonDebounceMillis >= BUTTON_DEBOUNCE_MS ) { if ( rawState != stableButtonState ) { stableButtonState = rawState; // ====================================================== // PRESSED // ====================================================== if (stableButtonState) { buttonPressStartMillis = now; longPressHandled = false; } // ====================================================== // RELEASED // ====================================================== else { // No long press -> normal short click if (!longPressHandled) { encoderButtonAction(); } } } } // ========================================================== // LONG PRESS // ========================================================== if ( stableButtonState && !longPressHandled && (now - buttonPressStartMillis) >= LONG_PRESS_MS ) { longPressHandled = true; resetTimer(); } } // ============================================================ // TIMER FINISHED ALARM // // RED + BUZZER = 500 ms // WHITE + OFF = 500 ms // // x3 // ============================================================ void flashFinished() { for (int i = 0; i < 3; i++) { // -------------------------------------------------------- // RED + BEEP // -------------------------------------------------------- renderFlashFrame( COL_RED ); buzzerON(); delay( 500 ); // -------------------------------------------------------- // WHITE + SILENT // -------------------------------------------------------- buzzerOFF(); renderFlashFrame( COL_WHITE ); delay( 500 ); } // Ensure final state buzzerOFF(); renderFlashFrame( COL_WHITE ); // ---------------------------------------------------------- // Re-sync encoder after blocking alarm // ---------------------------------------------------------- lastRawCLK = digitalRead( ENCODER_CLK ); stableCLK = lastRawCLK; clkChangeTime = millis(); lastClickTime = millis(); readyForNextClick = true; // Touch state reset touchWasDown = false; rangeTouchCandidate = false; } // ============================================================ // COUNTDOWN // ============================================================ void updateTimer() { if (!timerRunning) { return; } unsigned long now = millis(); if ( now - lastSecondMillis >= 1000UL ) { lastSecondMillis += 1000UL; if (remainingSeconds > 0) { remainingSeconds--; renderTimerFrame(); } // ======================================================== // FINISHED // ======================================================== if (remainingSeconds <= 0) { remainingSeconds = 0; timerRunning = false; countdownHasStarted = false; Serial.println( "TIMER FINISHED" ); flashFinished(); } } } // ============================================================ // DISPLAY INIT // ============================================================ void initDisplay() { pinMode( BL_PIN, OUTPUT ); digitalWrite( BL_PIN, HIGH ); panelBus = new Arduino_SWSPI( GFX_NOT_DEFINED, PANEL_CS, PANEL_SCK, PANEL_SDA, GFX_NOT_DEFINED ); rgbpanel = new Arduino_ESP32RGBPanel( 40, 7, 15, 41, 46, 3, 8, 18, 17, 14, 13, 12, 11, 10, 9, 5, 45, 48, 47, 21, 1, 50, 10, 50, 1, 30, 10, 30, PCLK_NEG, 8000000UL ); display = new Arduino_RGB_Display( DISPLAY_WIDTH, DISPLAY_HEIGHT, rgbpanel, 0, true, panelBus, GFX_NOT_DEFINED, st7701_type7_init_operations, sizeof( st7701_type7_init_operations ) ); // ---------------------------------------------------------- // Full screen Canvas // ---------------------------------------------------------- gfx = new Arduino_Canvas( DISPLAY_WIDTH, DISPLAY_HEIGHT, display ); bool ok = gfx->begin( 16000000 ); if (!ok) { Serial.println( "ERROR: CANVAS allocation failed!" ); while (true) { delay( 1000 ); } } } // ============================================================ // I2C + TOUCH + PCF INIT // ============================================================ void initTouchAndIO() { Wire.begin( I2C_SDA, I2C_SCL ); // PCF8574 is a standard-mode device. // CST8XX also works correctly at 100 kHz. Wire.setClock( 100000 ); // ---------------------------------------------------------- // External PCF: // all pins HIGH // buzzer therefore OFF // ---------------------------------------------------------- externalPcfState = 0xFF; externalPcfWrite(); // ---------------------------------------------------------- // Touch reset using INTERNAL PCF8574 // ---------------------------------------------------------- resetTouch(); // ---------------------------------------------------------- // Touch initialize // ---------------------------------------------------------- if ( !touch.begin( &Wire, TOUCH_ADDR ) ) { Serial.println( "ERROR: CST8XX TOUCH NOT FOUND" ); while (true) { delay( 1000 ); } } Serial.println( "TOUCH: OK" ); Serial.println( "EXTERNAL PCF8574: 0x20" ); Serial.println( "BUZZER: READY" ); } // ============================================================ // SETUP // ============================================================ void setup() { Serial.begin( 115200 ); // ---------------------------------------------------------- // Encoder // ---------------------------------------------------------- pinMode( ENCODER_CLK, INPUT_PULLUP ); pinMode( ENCODER_DT, INPUT_PULLUP ); lastRawCLK = digitalRead( ENCODER_CLK ); stableCLK = lastRawCLK; // ---------------------------------------------------------- // Display // ---------------------------------------------------------- initDisplay(); // ---------------------------------------------------------- // Touch + PCF8574 + buzzer // ---------------------------------------------------------- initTouchAndIO(); delay( 300 ); // ---------------------------------------------------------- // Initial screen // ---------------------------------------------------------- remainingSeconds = 0; buzzerOFF(); renderTimerFrame(); Serial.println(); Serial.println( "================================" ); Serial.println( "TIMER V1.0 FINAL READY" ); Serial.println( "================================" ); Serial.println( "1 MIN = YELLOW" ); Serial.println( "10 MIN = TURQUOISE" ); Serial.println( "30 MIN = ORANGE" ); Serial.println( "60 MIN = MAGENTA" ); Serial.println(); Serial.println( "SHORT CLICK = START / STOP" ); Serial.println( "LONG CLICK = RESET" ); Serial.println( "ALARM = 3 x RED + BEEP" ); } // ============================================================ // LOOP // ============================================================ void loop() { // Encoder first handleEncoder(); // Physical encoder button handleButton(); // Touch range selector handleTouch(); // Countdown updateTimer(); delay( 1 ); }